package fi.aalto.cse.T110_5150.P2PClient;

import java.io.*;
import java.util.*;
import java.util.Map.Entry;

public class Main {

	private static final String SYNTAX = "Syntax: \r\n";
	private static final String SYNTAX_AUTO_JOIN = "  autojoin on|off\r\n  autojoin print";
	private static final String SYNTAX_DEBUG = "  debug enable|disable all|join|query|heartbeat|networkprobing\r\n  debug print";
	private static final String SYNTAX_EXIT = "  exit";
	private static final String SYNTAX_INFO = "  info";
	private static final String SYNTAX_LISTEN = "  listen on|off\r\n  listen print";
	private static final String SYNTAX_PAUSE = "  pause";
	private static final String SYNTAX_PEER = "  peer connect <host>[:<port>]\r\n  peer disconnect all|<host>[:<port>]\r\n  peer print";
	private static final String SYNTAX_PRINT = "  print [<text>]";
	private static final String SYNTAX_QUERY = "  query <key>";
	private static final String SYNTAX_RUN = "  run [<filePath>]";
	private static final String SYNTAX_SHARE = "  share add <key> [<value1>[ <value2> [... <valueN>]]]\r\n  share remove <key>\r\n  share print";
	private static final String SYNTAX_START = "  start [<port>]";
	private static final String SYNTAX_HELP = "Supported commands: start|info|peer|share|query|exit|debug|print|autojoin|listen|pause|help|?;";

	private LocalPeer localPeer;
	private boolean printCommand;

	public Main(LocalPeer localPeer) {
		this.localPeer = localPeer;
	}

	/**
	 * @param args
	 */
	public static void main(String[] args) {

		try {

			LocalPeer localPeer = new LocalPeer();

			// read commands
			List<String> commands = null;

			if (args.length > 0) {

				try {
					commands = loadCommands(args[0]);

				} catch (P2PException e) {
					Logger.error(e);
					return;
				}
			}

			new Main(localPeer).run(commands);

		} catch (Exception e) {
			System.out.println("Unexpected error");
			e.printStackTrace();
			System.exit(-1);
		}

	}

	private void run(List<String> commands) throws IOException {

		if (commands != null) {

			// connect to remote peers in the initial list
			for (Iterator<String> it = commands.listIterator(); it.hasNext();) {
				handleCommand(it.next());
			}

		}

		// handle commands
		BufferedReader input = new BufferedReader(new InputStreamReader(System.in));

		//while (true) {
			String command = input.readLine();
			handleCommand(command);
		//}
	}

	private void handleCommand(String command) {

		try {
			
			int index;
			
			if ((index = command.lastIndexOf('\\')) >= 0) {
				command = command.substring(index + 1);
			}
			
//			if (printCommand) {
//				System.out.printf("==> %s%n", command);
//			}
			
			String[] commandArgs = getCommandArgs(command);

			if (commandArgs.length > 0) {
				if (commandArgs[0].equalsIgnoreCase("run")) {
					
					printCommand = true;

					try {
						handleCommandRun(commandArgs);
					} finally {
						printCommand = false;
					}

				} else if (commandArgs[0].equalsIgnoreCase("start")) {

					handleCommandStart(commandArgs);

				} else if (commandArgs[0].equalsIgnoreCase("peer")) {

					handleCommandPeer(commandArgs);

				} else if (commandArgs[0].equalsIgnoreCase("query")) {

					handleCommandQuery(commandArgs);

				} else if (commandArgs[0].equalsIgnoreCase("share")) {

					handleCommandShare(commandArgs);

				} else if (commandArgs[0].equalsIgnoreCase("info")) {

					printInfo();

				} else if (commandArgs[0].equalsIgnoreCase("debug")) {

					handleCommandDebug(commandArgs);

				} else if (commandArgs[0].equalsIgnoreCase("help") || commandArgs[0].equalsIgnoreCase("?")) {

					System.out.println(SYNTAX_HELP);
					System.out.println(SYNTAX_AUTO_JOIN);
					System.out.println(SYNTAX_DEBUG);
					System.out.println(SYNTAX_EXIT);
					System.out.println(SYNTAX_INFO);
					System.out.println(SYNTAX_LISTEN);
					System.out.println(SYNTAX_PAUSE);
					System.out.println(SYNTAX_PEER);
					System.out.println(SYNTAX_PRINT);
					System.out.println(SYNTAX_QUERY);
					System.out.println(SYNTAX_RUN);
					System.out.println(SYNTAX_SHARE);
					System.out.println(SYNTAX_START);
					System.out.println();

				} else if (commandArgs[0].equalsIgnoreCase("exit")) {

					handleCommand("peer disconnect all");
					Logger.info("Stopped");
					System.exit(0);

				} else if (commandArgs[0].equalsIgnoreCase("print")) {

					System.out.println(command.substring("print".length()).trim());

				} else if (commandArgs[0].equalsIgnoreCase("autojoin")) {

					handleCommandAutoJoin(commandArgs);

				} else if (commandArgs[0].equalsIgnoreCase("listen")) {

					handleCommandListen(commandArgs);

				} else if (commandArgs[0].equalsIgnoreCase(";") || commandArgs[0].isEmpty() ) {
					
					// do nothing
					
				} else if (commandArgs[0].equalsIgnoreCase("pause")) {

					handleCommandPause(commandArgs);

				} else {
					throw new InvalidPropertiesFormatException(
							String.format("Invalid command '%s'. Use command 'help' or '?' to see supported commands",
									commandArgs[0]));
				}
			}
		} catch (InvalidPropertiesFormatException e) {
			System.out.println(e.getMessage());
		} catch (P2PException e) {
			Logger.error(e.getMessage());
		}
	}

	private static List<String> loadCommands(String filePath) throws P2PException {

		FileReader fileReader = null;

		try {
			fileReader = new FileReader(filePath);

			BufferedReader bufferedReader = new BufferedReader(fileReader);

			// read commands
			List<String> commands = new ArrayList<String>();

			for (String line; (line = bufferedReader.readLine()) != null;) {

				line = line.trim();

				if (!line.isEmpty()) {
					commands.add(line);
				}
			}

			return commands;

		} catch (IOException e) {
			String message = String.format("Could not read file '%s'", filePath);
			throw new P2PException(message, e);
		} finally {
			if (fileReader != null) {
				try {
					fileReader.close();
				} catch (IOException e) {
				}
			}
		}

	}

	private void handleCommandRun(String[] commandArgs) throws P2PException, InvalidPropertiesFormatException {
		if (commandArgs.length == 2) {
			List<String> commands = loadCommands(commandArgs[1]);

			if (commands != null) {

				// connect to remote peers in the initial list
				for (Iterator<String> it = commands.listIterator(); it.hasNext();) {
					handleCommand(it.next());
				}

			}

		} else {
			throw new InvalidPropertiesFormatException(SYNTAX + SYNTAX_RUN);
		}
	}

	private void handleCommandStart(String[] commandArgs) throws P2PException {

		// default port number
		int port = P2PHelper.PORT_DEFAULT;

		if (commandArgs.length == 2) {
			try {
				port = Integer.parseInt(commandArgs[1]);
			} catch (NumberFormatException e) {
				throw new P2PException(String.format("Invalid port number %s", commandArgs[1]));
			}
		}

		localPeer.initialize(port);
	}

	private void handleCommandQuery(String[] commandArgs) throws InvalidPropertiesFormatException, P2PException {
		if (commandArgs.length != 2) {
			throw new InvalidPropertiesFormatException(SYNTAX + SYNTAX_QUERY);
		}

		ByteArray queryKey = new ByteArray();
		queryKey.decode(commandArgs[1]);
		localPeer.sendMessageQuery(queryKey);
	}

	private void handleCommandPeer(String[] commandArgs) throws InvalidPropertiesFormatException, P2PException {
		if (commandArgs.length == 2 && commandArgs[1].equalsIgnoreCase("print")) {
			printRemotePeerAddresses();
			return;
		}

		if (commandArgs.length > 1) {

			if (commandArgs[1].equalsIgnoreCase("connect")) {
				if (commandArgs.length != 3) {
					throw new InvalidPropertiesFormatException(SYNTAX + SYNTAX_PEER);
				}

				PeerAddress remotePeerAddress = PeerAddress.parse(commandArgs[2]);
				localPeer.connect(remotePeerAddress);
				return;
			} else if (commandArgs[1].equalsIgnoreCase("disconnect")) {
				if (commandArgs.length != 3) {
					throw new InvalidPropertiesFormatException(SYNTAX + SYNTAX_PEER);
				}

				if (!commandArgs[2].equalsIgnoreCase("all")) {
					PeerAddress remotePeerAddress = PeerAddress.parse(commandArgs[2]);
					localPeer.disconnect(remotePeerAddress);
				} else {
					for (Enumeration<PeerAddress> en = localPeer.getRemotePeers().keys(); en.hasMoreElements();) {
						localPeer.disconnect(en.nextElement());
					}
				}

				return;
			}
		}

		throw new InvalidPropertiesFormatException(SYNTAX + SYNTAX_PEER);
	}

	private void handleCommandShare(String[] commandArgs) throws InvalidPropertiesFormatException, P2PException {

		if (commandArgs.length == 2 && commandArgs[1].equalsIgnoreCase("print")) {
			printShares();
			return;
		}

		if (commandArgs.length <= 3) {
			throw new InvalidPropertiesFormatException(SYNTAX + SYNTAX_SHARE);
		}

		String action = commandArgs[1];
		ByteArray keyArray = new ByteArray();
		keyArray.decode(commandArgs[2]);

		if (action.equalsIgnoreCase("add")) {
			if (commandArgs.length != 4) {
				throw new InvalidPropertiesFormatException(SYNTAX + SYNTAX_SHARE);
			}

			if (!localPeer.getSharedResources().containsKey(keyArray)) {

				IntegerArray valueArray = new IntegerArray();
				valueArray.decode(commandArgs[3]);

				localPeer.getSharedResources().setValues(keyArray, valueArray);
				System.out.printf("Key '%s' is added%n", keyArray);
			} else {
				throw new P2PException(String.format("Key '%s' is duplicated", keyArray));
			}

		} else if (action.equalsIgnoreCase("remove")) {

			if (localPeer.getSharedResources().remove(keyArray) != null) {
				System.out.printf("Key '%s' is removed%n", keyArray);
			} else {
				throw new P2PException(String.format("Key '%s' is not found", keyArray));
			}

		} else {
			throw new InvalidPropertiesFormatException(SYNTAX + SYNTAX_SHARE);
		}
	}

	private void handleCommandDebug(String[] commandArgs) throws InvalidPropertiesFormatException {

		boolean print = false;

		if (commandArgs.length >= 3) {

			int category = Logger.NONE;

			for (int i = 2; i < commandArgs.length; ++i) {
				if (commandArgs[i].equalsIgnoreCase("all")) {
					category |= Logger.ALL;
				} else if (commandArgs[i].equalsIgnoreCase("join")) {
					category |= Logger.JOIN;
				} else if (commandArgs[i].equalsIgnoreCase("query")) {
					category |= Logger.QUERY;
				} else if (commandArgs[i].equalsIgnoreCase("heartbeat")) {
					category |= Logger.HEART_BEAT;
				} else if (commandArgs[i].equalsIgnoreCase("networkprobing")) {
					category |= Logger.NETWORK_PROBING;
				} else {
					throw new InvalidPropertiesFormatException(SYNTAX + SYNTAX_DEBUG);
				}
			}

			if (commandArgs[1].equalsIgnoreCase("enable")) {
				Logger.enableDebug(category);
				print = true;
			} else if (commandArgs[1].equalsIgnoreCase("disable")) {
				Logger.disableDebug(category);
				print = true;
			} else {
				throw new InvalidPropertiesFormatException(SYNTAX + SYNTAX_DEBUG);
			}
		}

		if (print || (commandArgs.length == 2 && commandArgs[1].equalsIgnoreCase("print"))) {

			System.out.printf("Enabled debug categories: %s.%n", Logger.getEnabledDebugCategoryNames());
			
		} else {
			throw new InvalidPropertiesFormatException(SYNTAX + SYNTAX_DEBUG);
		}

	}

	private void handleCommandAutoJoin(String[] commandArgs) throws InvalidPropertiesFormatException {
		boolean print = false;
		if (commandArgs.length == 2) {
			if (commandArgs[1].equalsIgnoreCase("on")) {
				localPeer.setAutoJoin(true);
				print = true;
			} else if (commandArgs[1].equalsIgnoreCase("off")) {
				localPeer.setAutoJoin(false);
				print = true;
			}

			if (print || commandArgs[1].equalsIgnoreCase("print")) {
				System.out.printf("Autojoin is turned %s%n", localPeer.getAutoJoin() ? "on" : "off");
				return;
			}
		}

		throw new InvalidPropertiesFormatException(SYNTAX + SYNTAX_AUTO_JOIN);
	}

	private void handleCommandListen(String[] commandArgs) throws InvalidPropertiesFormatException {
		boolean print = false;
		if (commandArgs.length == 2) {
			if (commandArgs[1].equalsIgnoreCase("on")) {
				localPeer.setListen(true);
				print = true;
			} else if (commandArgs[1].equalsIgnoreCase("off")) {
				localPeer.setListen(false);
				print = true;
			}

			if (print || commandArgs[1].equalsIgnoreCase("print")) {
				System.out.printf("Listening is turned %s%n", localPeer.getListen() ? "on" : "off");
				return;
			}
		}

		throw new InvalidPropertiesFormatException(SYNTAX + SYNTAX_AUTO_JOIN);
	}

	private void handleCommandPause(String[] commandArgs) {
		System.out.println("==> Press Enter to continue...");		
		try {
			while (System.in.available() == 0) {				
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
				}
			}
		} catch (IOException e) {
		}
	}

	private void printInfo() {
		System.out.println("Peer info:");
		System.out.printf("  Peer address:              %s%n", localPeer);
		System.out.printf("  Number of connected peers: %d%n", localPeer.getRemotePeers().size());
		System.out.printf("  Number of shared keys:     %d%n", localPeer.getSharedResources().size());
		System.out.printf("  Listening:	             %s%n", localPeer.getListen() ? "on" : "off");
		System.out.printf("  Auto join:	             %s%n", localPeer.getAutoJoin() ? "on" : "off");
		System.out.printf("  Debug categories:          %s%n", Logger.getEnabledDebugCategoryNames());
		printSeparator();
	}

	private void printRemotePeerAddresses() {
		Hashtable<PeerAddress, RemotePeer> remotePeers = localPeer.getRemotePeers();
		System.out.printf("List of connected peers (%d):%n", remotePeers.size());
		for (Enumeration<PeerAddress> enumeration = remotePeers.keys(); enumeration.hasMoreElements();) {
			System.out.printf("  %s%n", enumeration.nextElement());
		}
		printSeparator();
	}

	private void printShares() {
		SharedResources sharedResources = localPeer.getSharedResources();
		System.out.printf("List of share values (%d keys):%n", sharedResources.size());
		for (Iterator<Entry<ByteArray, Resource[]>> it = sharedResources.entrySet().iterator(); it.hasNext();) {
			Entry<ByteArray, Resource[]> entry = it.next();
			System.out.printf("  %s =", entry.getKey());
			Resource[] resources = entry.getValue();
			for (int i = 0; i < resources.length; ++i) {
				System.out.printf(" %s", resources[i]);
			}
			System.out.println();
		}
		printSeparator();		
	}

	private static String[] split(String s) {
		s = s.trim();
		if (!s.isEmpty()) {
			return s.split(" ");
		} else {
			return new String[] {};
		}
	}

	private static String[] getCommandArgs(String s) throws P2PFormatException {
		
		s = s.trim();		
		
		int index, start, end;		

		ArrayList<String> commandArgs = new ArrayList<String>();


		for (index = 0; (start = s.indexOf('{', index)) >= 0; index = end + 1) {

			String[] commandArgs2 = split(s.substring(index, start));

			for (int i = 0; i < commandArgs2.length; ++i) {
				commandArgs.add(commandArgs2[i]);
			}

			end = s.indexOf('}', start + 1);

			if (end < 0) {
				throw new P2PFormatException();
			}

			commandArgs.add(s.substring(start, end + 1));
		}

		String[] commandArgs2 = split(s.substring(index));

		for (int i = 0; i < commandArgs2.length; ++i) {
			commandArgs.add(commandArgs2[i]);
		}

		String[] result = new String[commandArgs.size()];

		return commandArgs.toArray(result);
	}
	
	private void printSeparator() {
		System.out.println("===================================");		
	}

}
